'''
Created by Piotr Duda <piotr.duda _AT_ polidea.pl>
for Polidea
'''

import os
import sys
import inspect
import logging

from google.appengine.ext import db

from strings import java_assignment, java_class, java_constructor, \
                    java_db_variable_dec, java_getter, java_setter, \
                    java_variable_dec, java_variable_decl, java_list_decl\



def camel_case( variable_name ):
    '''
    Convert from Python's notation to Java's camelCase notation
    '''
    if not variable_name.isupper():
        pos = variable_name.find( '_' )
        while pos > 0:
            variable_name = variable_name[0:pos] + variable_name[pos + 1:].capitalize()
            pos = variable_name.find( '_' )
    return variable_name


class CodeObject( object ):
    '''
    Basic class for all the code objects of a Python's source file
    '''
    def to_java( self ):
        raise Exception( "Not implemented" )

class PythonVariable( CodeObject ):
    '''
    This class represents a python's class attribute.
    '''

    basic_types = {
        'str'     : 'String',
        'int'     : 'int',
        'float'   : 'float',
        'bool'    : 'boolean',
    }

    property_suffix = 'Property'
    unknown_type_msg = '/*TODO:Unknown Type */'

    def __init__( self, vname, vtype, value ):
        '''
        Creates a new PythonVariable from arguments: 
        vname - variables name 
        vtype - variables type
        value - variables value
        '''
        CodeObject.__init__( self )
        self.name = vname
        self.type = vtype
        self.value = value
        self.printable_name = camel_case( vname )

    def print_value( self ):
        '''
        Creates a printable representation of variables value 
        '''
        value = repr( self.value )
        return value

    def java_declaration( self ):
        '''
        Returns variable's Java declaration. The declaration depends on the type of the value:
        - For basic types nonpersistent private variable declarations will be returned.
        - For google types persistent private variable declarations will be returned.
        - For variables of basic types with a defined value - the same value will be assigned
        - For list object with default values, static initialization will be used
        '''
        code = ''
        if PythonVariable.is_persistent( self.type ):
            code = java_db_variable_dec % ( self.get_java_type( self.type, self.value ), self.printable_name )
        elif self.value == None:
            code = java_variable_dec % ( self.get_java_type( self.type, self.value ), self.printable_name )
        else :
            if self.type == list.__name__:
                values = "".join( self.print_value() ).replace( "[", "" ).replace( "]", "" ).replace( "'", "\"" )
                code = java_list_decl % ( self.get_java_type( self.type, self.value ), self.printable_name,
                                          self.get_java_type( self.type, self.value ), values )
            else:
                code = java_variable_decl % ( "static final " + self.get_java_type( self.type, self.value ), self.printable_name, self.print_value() )

        return code

    def java_func_attr( self ):
        '''
        Returns variables Java type and name in format "Type VariableName" 
        suitable for putting as function arguments
        '''
        return  "%s %s" % ( self.get_java_type( self.type, self.value ),
                            self.printable_name )

    def is_list( self ):
        '''
        Checks whether PythonVariable object is a list
        '''
        return PythonVariable.get_java_type( self.type, self.value )\
                .find( "List" ) != -1

    @staticmethod
    def get_java_type( vtype, value ):
        '''
        Gets Java type for a variable. The "value" argument is necessary for
        determining types of nested variables ( References,Lists,Dictionaries)
        This function is recursive ( for complex types ). 
        '''
        def _inner_type_name( value ):
            '''
            Returns the inner type for the complex type "value". This function
            succeeds only if 'value' is populated ( otherwise there is no way
            to determine the type and "unknown type" string is returned). 
            '''
            if len( value ) > 0:
                return PythonVariable.get_java_type( type( value[0] ).__name__, value[0] )
            else:
                return PythonVariable.unknown_type_msg

        if vtype == list.__name__:
            return 'List<%s>' % _inner_type_name( value )

        elif vtype == dict.__name__:
            first_type = _inner_type_name( value.keys() )
            second_type = _inner_type_name( value.values() )
            return 'HashMap<%s,%s>' % ( first_type, second_type )

        elif PythonVariable.basic_types.has_key( vtype ):
            return PythonVariable.basic_types[vtype]

        elif PythonVariable.is_persistent( vtype ):
            return PythonVariable._translate_google_type( vtype, value )
        else :
            return type

    @staticmethod
    def _translate_google_type( vtype, value ):
        '''
        Translates Python's google type to a Java Type 
        '''
        name = vtype.split( '.' )[-1].replace( PythonVariable.property_suffix, '' )
        if name == 'Reference':
            return value.data_type.__name__

        if name == 'List':
            type_name = PythonVariable.unknown_type_msg
            if value.item_type.__name__ != 'Key':
                type_name = type( value ).__name__

            return "List<%s>" % type_name

        elif name == 'StringList':
            return "List<String>"

        elif name == 'DateTime':
            return 'Date'

        elif name == 'Float' or name == 'Boolean':
            return name.lower()

        elif name == 'Integer':
            return "int"

        else :
            return name


    @staticmethod
    def is_persistent( var_type ):
        '''
        Checks whether PythonVariable instance is of "google's" type
        ( all the classes with "Property" in the name.
        '''
        return True if var_type.find( PythonVariable.property_suffix ) != -1\
                    else False


class PythonClass( CodeObject ):
    '''
    This class represents a python's class.
    '''
    def __init__( self, ctype, ccode = '', cdoc = '' ):
        '''
        Creates a new PythonClass from arguments:  
        ctype - class type object
        ccode - class source code
        cdoc - class documentation string
        '''
        CodeObject.__init__( self )
        self.type = ctype
        self.code = ccode
        self.doc = cdoc
        self.variables = self.get_class_variables()

    def get_class_variables( self ):
        '''
        Returns a list of all the PythonVariable objects associated with the
        parsed class. 
        '''
        objects = []
        for var in self.get_sorted_variable_names():
            obj_type = type( getattr( self.type, var ) ).__name__
            value = getattr( self.type, var )
            objects.append( PythonVariable( var, obj_type, value ) )
        return objects

    def get_sorted_variable_names( self ):
        '''
        Get all the variable names in the parsed class. Sort the variable list
        so as constants are first ( upper case names = constants )
        '''
        # Don't take internal variables and ReverseReferences
        variables = [ elem  for elem in self.type.__dict__\
                 if not elem.startswith( '_' ) and not elem.endswith( '_set' )]
        variables.sort( lambda x, y:-1 if x.isupper() else 0 )
        return variables

    def create_getters( self ):
        '''
        Create getters for all mutable attributes of the parsed class. Returns 
        Java getter's code.
        '''
        code = ''
        for attr in self.variables:
            java_type = PythonVariable.get_java_type( attr.type, attr.value )
            if not attr.name.isupper():
                code = code + ( java_getter % ( java_type,
                        attr.printable_name.capitalize(), attr.printable_name ) )
        return code


    def create_setters( self ):
        '''
        Create setters for all mutable attributes of the parsed class. Returns 
        Java setter's code.
        '''
        code = ''
        for attr in self.variables:
            if not attr.name.isupper() and not attr.is_list() :
                code = code + ( java_setter % ( attr.printable_name.capitalize(),
                            attr.get_java_type( attr.type, attr.value ), attr.printable_name, attr.printable_name,
                            attr.printable_name ) )
        return code

    def create_constructor( self ):
        '''
        Create the constructor for the parsed class.
        '''
        fun_args = ', '.join( [  attr.java_func_attr() for attr in\
                                self.variables if not attr.name.isupper()] )

        assignments = ''.join( [ java_assignment % ( attr.printable_name, \
                                attr.printable_name ) for attr in
                                self.variables  if not attr.name.isupper()] )

        return java_constructor % ( self.type.__name__, fun_args, assignments )

    def to_java( self ):
        '''
        Translate PythonClass to java source code
        '''
        attr_code = ''.join( [ var.java_declaration() for var in self.variables] )
        imports = 'import com.google.appengine.api.datastore.*;'
        constr_code = self.create_constructor()
        get_code = self.create_getters()
        set_code = self.create_setters()
        return java_class % ( imports, self.type.__name__, attr_code, constr_code,
                get_code, set_code )

class PythonModule( CodeObject ):
    '''
    This class represents a python's module.
    '''
    model_classes = ['Model', 'Expando', 'PolyModel']

    def __init__( self, name ):
        '''
        Creates PythonModule object by importing "name" module. All the 
        datastore classes are found and parsed.
        '''
        CodeObject.__init__( self )
        self.module = __import__( name.split( '.' )[0] )
        self.classes = self.get_model_classes()

    def get_model_classes( self ):
        '''
        Gets all the datastore classes from given python module - classes
        of different types are ignored.
        '''
        classes = []
        for name, pyclass in inspect.getmembers( self.module, inspect.isclass ):
            logging.debug( "Found:%s model class", name )
            if PythonModule.get_superclass( pyclass ).__name__ in self.model_classes:
                classes.append( PythonClass( pyclass,
                    inspect.getsource( pyclass ),
                    inspect.getdoc( pyclass ) ) )
        return classes

    @staticmethod
    def get_superclass( pyclass ):
        '''
        Get's the superclass of a given python class.
        '''
        return pyclass.__bases__[0]



def main():
    if len( sys.argv ) < 3:
        sys.exit( 'Usage: %s input_file.py output_dir' % sys.argv[0] )

    python_file = sys.argv[1] if sys.argv[1].endswith( '.py' )\
        else sys.exit( 'Bad input file' )

    directory = sys.argv[2] if os.path.lexists( sys.argv[2] )\
        else sys.exit( 'Specified path does not exist' )

    print "Converting %s to java classes" % python_file
    for cl in PythonModule( python_file ).classes:
        print "Saving class:%s" % cl.type.__name__
        location = os.path.join( directory, cl.type.__name__ + ".java" )
        fp = open( location, 'w' )
        fp.write( cl.to_java() )
        fp.close()


if __name__ == '__main__':
    logging.getLogger().setLevel( logging.NOTSET )
    main()




